/**
*    Copyright (C) 2008  Chase Kernan 
*    chase.kernan@gmail.com
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
* 
*/

package com.chasekernan.hxnova.core.dataholders;

import com.chasekernan.hxnova.core.fleets.Fleet;
import com.chasekernan.hxnova.utils.Utils;
import com.chasekernan.hxnova.utils.Vector;
import com.chasekernan.hxnova.core.players.Player;

/**
    This cannot extend [ParticleHolder] as fleets need to be stored by player
    in order to handle splitting/merging of fleets easily. Otherwise, this 
    mimics the api of [ParticleHolder].
**/
class FleetHolder {
    
    /**
        Each player can only have up to this many fleets.
    **/
    public static var MAX_FLEETS_PER_PLAYER = 512;
    
    /**
        Fleets are stored by their owner's id.
        This is an [IntHash] because the null player may have fleets and its id
        is -1.
    **/
    public var particles : IntHash < Array < Fleet >> ;
    
    /**
        [Global.players] needs to be initialized before this.
    **/
    public function new() {
        if (Global.players == null) {
            throw "Global.players needs to be initialized before a " +
                  "FleetHolder can be initialized.";
        }
        
        particles = new IntHash();
        
        for (player in Global.players) {
            particles.set(player.id, new Array<Fleet>());
        }
    }
    
    /**
        Returns the fleet with the given id and given owner (null if the fleet 
        no longer exists).
        Will throw an error if the id is out of range or the owner id is out of
        range.
    **/
    public function getById(fleetID : Int, ownerID : Int) : Fleet {
        if (!particles.exists(ownerID)) throw "Owner id is out of range.";
        
        var playerFleets = particles.get(ownerID);
        
        if (fleetID < 0 || fleetID >= MAX_FLEETS_PER_PLAYER) {
            throw "Fleet ID is out of range.";
        }
        
        if (fleetID >= playerFleets.length) return null;
        
        return playerFleets[fleetID];
    }
    
    /**
        If [ownerID] isn't specified, then the fleet is added to its owner's 
        list of fleets. If the fleet's owner is null, then an error is thrown.
        
        If it is specified, then the fleet is added to the specified player's
        list of fleets. If the given id is out of range, then an error is 
        thrown.
        
        Will throw an error if the maximum number of fleets has been reached 
        for the given player.
        TODO: Maybe just ignore the call?
    **/
    public function appendParticle(f : Fleet, ?ownerID : Int) {
        //cast here as f.owner is Null<Int> while ownerID is Int.
        var id : Int = untyped 
        if (ownerID == null) {
            if (f.owner == null || f.owner.id == null) {
                throw "If owner id isn't specified, then the fleet owner " +
                      "cannot be null.";
            }
            
            f.owner.id;
        } else ownerID;
        
        if (!particles.exists(id)) throw "Owner id is out of range";
        
        var fleets = particles.get(id);
        
        //if a suitable id cannot be found, then max fleets has been reached
        for (possibleID in 0...MAX_FLEETS_PER_PLAYER) {
            if (fleets[possibleID] == null) {
                f.id = possibleID;
                fleets[possibleID] = f;
                return;
            }
        }
        
        //if here, then there are no slots open
        throw "Player " + id + " has reached the maximum number of fleets.";
    }
    
    /**
        Adds the fleet to its owner's list using its id. It will overwrite any 
        existing fleet stored there.
        Will throw an error if the id is out of range, the fleet is null, the
        fleet id is null, or its owner/owner id is null.
    **/
    public function addParticle(f : Fleet) {
        if (f == null) throw "Fleet can't be null.";
        if (f.id == null) throw "Fleet id can't be null.";
        if (f.id < 0 || f.id >= MAX_FLEETS_PER_PLAYER) {
            throw "Fleet id is out of range";
        }
        if (f.owner == null || f.owner.id == null) {
            throw "Fleet owner cannot be null.";
        }
        if (!particles.exists(f.owner.id)) {
            throw "Fleet owner id is out of range.";
        }
        
        particles.get(f.owner.id)[f.id] = f;
    }
    
    /**
        Sets the array element corresponding to the fleet's id to null.
        Will throw an error if the fleet is null, the fleet id is null, the
        fleet's id is out of range, or the fleet's owner/owner id is null.
    **/
    public function removeParticle(f : Fleet) {
        if (f == null) throw "Fleet can't be null.";
        if (f.id == null) throw "Fleet id can't be null.";
        if (f.id < 0 || f.id >= MAX_FLEETS_PER_PLAYER) {
            throw "Fleet id is out of range";
        }
        if (f.owner == null || f.owner.id == null) {
            throw "Fleet owner cannot be null.";
        }
        if (!particles.exists(f.owner.id)) {
            throw "Fleet owner id is out of range.";
        }
        
        particles.get(f.owner.id)[f.id] = null;
    }
    
    public function iterator() {
        return getAllFleets().iterator();
    }
    
    /**
        Returns an array of all the fleets, regardless of owner.
    **/
    public function getAllFleets() : Array < Fleet > {
        var allFleets = new Array<Fleet>();
        for (fleetSet in particles) {
            allFleets = allFleets.concat(fleetSet.copy());
        }
        return allFleets;
    }
    
    /**
        Returns all of the fleets at the given location.
        If a player is given, it only returns the fleets of that player.
        
        This will throw an error if the player id is null, the id is out of 
        range, or the location is null.
    **/
    public function getParticlesAtLocation(location : Vector, ?player : Player) 
                    : Array <Fleet> {
        if (location == null) throw "Location cannot be null.";
        
        var fleetsAtLocation = new Array<Fleet>();
        
        if (player != null) {
            if (player.id == null) throw "Player id cannot be null.";
            
            var fleets = particles.get(player.id);
            if (fleets == null) throw "Player id is out of range.";
            
            for (fleet in fleets) {
                if (location.equals(fleet.location)) {
                    fleetsAtLocation.push(fleet);
                }
            }
            
        } else {
            for (fleetSet in particles) {
                for (fleet in fleetSet) {
                    if (location.equals(fleet.location)) {
                        fleetsAtLocation.push(fleet);
                    }
                }
            }
        }
        
        return fleetsAtLocation;
    }
    
    public static var XML_TITLE = "fleets";
    
    /**
        Writes a set of fleets to an xml file.
        Sample xml:
            TODO: run [writeToXml];
    **/
    public static function writeToXml(fh : FleetHolder) : Xml {
        var xml = Xml.parse('<' + XML_TITLE + '></' + XML_TITLE + '>')
                .firstChild();
        
        for (ownerID in fh.particles.keys()) {
            var fleetsXml = Xml.parse("<player_fleets></player_fleets>")
                            .firstChild();
            fleetsXml.set("player_id", Std.string(ownerID));
            
            var fleets = fh.particles.get(ownerID);
            for (fleet in fleets) {
                if (fleet == null) continue;
                
                fleetsXml.addChild(Fleet.writeToXml(fleet));
                fleetsXml.addChild(Xml.createElement("spacer"));
            }
            
            xml.addChild(fleetsXml);
            xml.addChild(Xml.createElement("spacer"));
        }
        
        return xml;
    }
    
    /**
        Reads a set of fleets from xml.
        Will throw an error if there are any mistakes.
    **/
    public static function readFromXml(xml : Xml) : FleetHolder {
        try {
            var fh = new FleetHolder();
            
            for (fleetsXml in xml.elementsNamed("player_fleets")) {
                for (fleetXml in fleetsXml.elementsNamed(Fleet.XML_TITLE)) {
                    var f = Fleet.readFromXml(fleetXml);
                    fh.addParticle(f);
                }
            }
            
            return fh;
        } catch (e : Dynamic) {
            throw "Error while reading from fleet holder xml: " + e;
        }
    }
    
    /**
        Copies the structure holding the fleets, but not the fleet data itself.
    **/
    public function clone() : FleetHolder {
        var fh = new FleetHolder();
        
        for (key in particles.keys()) {
            fh.particles.set(key, particles.get(key).copy());
        }
        
        return fh;
    }
}